<?php

/*
 * This file is part of jwt-auth.
 *
 * (c) Sean Tymon <tymon148@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Trip\Auth\JWT\Providers\JWT;

use App\Trip\Auth\JWT\Contracts\Providers\JWT;
use App\Trip\Auth\JWT\Exceptions\JWTException;
use App\Trip\Auth\JWT\Exceptions\TokenInvalidException;
use DateTimeImmutable;
use Exception;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Ecdsa;
use Lcobucci\JWT\Signer\Ecdsa\Sha256 as ES256;
use Lcobucci\JWT\Signer\Ecdsa\Sha384 as ES384;
use Lcobucci\JWT\Signer\Ecdsa\Sha512 as ES512;
use Lcobucci\JWT\Signer\Hmac\Sha256 as HS256;
use Lcobucci\JWT\Signer\Hmac\Sha384 as HS384;
use Lcobucci\JWT\Signer\Hmac\Sha512 as HS512;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa;
use Lcobucci\JWT\Signer\Rsa\Sha256 as RS256;
use Lcobucci\JWT\Signer\Rsa\Sha384 as RS384;
use Lcobucci\JWT\Signer\Rsa\Sha512 as RS512;
use Lcobucci\JWT\Validation\Constraint\PermittedFor;
use ReflectionClass;
use ReflectionException;

class Lcobucci extends Provider implements JWT
{

    /**
     * Signers that this provider supports.
     *
     * @var array
     */
    protected array       $signers
        = [
            'HS256' => HS256::class,
            'HS384' => HS384::class,
            'HS512' => HS512::class,
            'RS256' => RS256::class,
            'RS384' => RS384::class,
            'RS512' => RS512::class,
            'ES256' => ES256::class,
            'ES384' => ES384::class,
            'ES512' => ES512::class,
        ];
    private Signer        $signer;
    private Configuration $config;

    /**
     * Create the Lcobucci provider.
     *
     * @param string $secret
     * @param string $algo
     * @param array  $keys
     *
     * @throws JWTException
     */
    public function __construct(
        string $secret,
        string $algo,
        array  $keys
    )
    {
        parent::__construct($secret, $algo, $keys);
        $this->signer = $this->getSigner();

        $key          = InMemory::base64Encoded($secret);
        $this->config = Configuration::forSymmetricSigner(
            $this->signer,
            $key
        );
    }

    /**
     * Get the signer instance.
     *
     * @return Signer
     * @throws JWTException
     *
     */
    protected function getSigner(): Signer
    {
        if (!array_key_exists($this->algo, $this->signers)) {
            throw new JWTException('The given algorithm could not be found');
        }

        return new $this->signers[$this->algo];
    }

    /**
     * Create a JSON Web Token.
     *
     * @param array $payload
     *
     * @return string
     *
     */
    public function encode(array $payload): string
    {
        $now     = new DateTimeImmutable();
        $builder = $this->config->builder();
        $token   = $builder
            ->permittedFor($payload['iss'])
            ->issuedAt($now->setTimestamp($payload['iat']))
            ->canOnlyBeUsedAfter($now->setTimestamp($payload['nbf']))
            ->identifiedBy($payload['jti'])
            ->expiresAt($now->setTimestamp($payload['exp']))
            ->issuedBy($payload['iss'])
            ->relatedTo($payload['sub'])
            ->withClaim('prv', $payload['prv'])
            ->getToken($this->config->signer(), $this->config->signingKey());
        return $token->toString();
    }

    /**
     * Decode a JSON Web Token.
     *
     * @param string $token
     *
     * @return array
     * @throws JWTException
     *
     */
    public function decode(string $token): array
    {
        try {
            $jwt = $this->config->parser()->parse($token);
        } catch (Exception $e) {
            throw new TokenInvalidException('token is invalid');
        }
        try {
            $auds         = $jwt->claims()->get('aud') ?: [];
            $PermittedFor = [];

            foreach ($auds as $aud) {
                $PermittedFor[] = new PermittedFor($aud);
            }
            $this->config->setValidationConstraints(
                ...$PermittedFor
            );

            $constraints = $this->config->validationConstraints();
            if (!$this->config->validator()->validate($jwt, ...$constraints)) {
                throw new TokenInvalidException('Token Signature could not be verified.');
            }
            return $jwt->claims()->all();
        } catch (Exception $e) {
            throw new TokenInvalidException('token is invalid');
        }
    }

    /**
     * {@inheritdoc}
     * @throws ReflectionException
     */
    protected function getVerificationKey()
    {
        if ($this->isAsymmetric()) {
            if (!$public = $this->getPublicKey()) {
                throw new JWTException('Public key is not set.');
            }

            return $this->getKey($public);
        }

        if (!$secret = $this->getSecret()) {
            throw new JWTException('Secret is not set.');
        }

        return $this->getKey($secret);
    }

    /**
     * {@inheritdoc}
     * @throws ReflectionException
     */
    protected function isAsymmetric(): bool
    {
        $reflect = new ReflectionClass($this->signer);

        return $reflect->isSubclassOf(Rsa::class) || $reflect->isSubclassOf(Ecdsa::class);
    }

    /**
     * Get the signing key instance.
     */
    protected function getKey(string $contents, string $passphrase = ''): Key
    {
        return InMemory::plainText($contents, $passphrase);
    }

    /**
     * {@inheritdoc}
     * @throws ReflectionException
     */
    protected function getSigningKey()
    {
        if ($this->isAsymmetric()) {
            if (!$privateKey = $this->getPrivateKey()) {
                throw new JWTException('Private key is not set.');
            }

            return $this->getKey($privateKey, $this->getPassphrase() ?? '');
        }

        if (!$secret = $this->getSecret()) {
            throw new JWTException('Secret is not set.');
        }

        return $this->getKey($secret);
    }
}
